ViewPager 刷新无效?
点击上方“鸿洋”,选择“置顶公众号”
优质技术文第一时间送达
本文作者
作者:川峰
链接:
https://blog.csdn.net/lyabc123456/article/details/79797552
本文由作者授权发布。
最近在重构项目的时候有个地方想要做一个更换FragmentPagerAdapter中的Fragment的功能,按照通常使用ListView的习惯做法,如果你只是更新保存Fragment的List数据,然后调用adapter的notifyDataSetChanged()是不会起作用的(下面会分析原因)。
搜索了下发现此问题普遍存在,多数是说先移除Fragment再notifyDataSetChanged(),因为FragmentPagerAdapter内部会缓存Fragment,但是经测试发现仅仅这样干是不行的。于是经过一番折腾,参考了各种方案之后我整理了一个可行的方案,本文做一个记录,以便后续参考,也方便各位道友参考。
下面来分析一下此问题的主要原因:
这可能是Android一个BUG, 与此问题相关的主要有两个方法:
getItemPosition()
instantiateItem()
搞清楚这两个方法的作用基本就知道如何解决了,先来看第一个方法
这个是在PagerAdapter中的getItemPosition()源码的说明,从它的英文注释我们可以清楚的知道,这个方法的返回值的意思是:如果给定的item的position没有发生改变,那么就返回POSITION_UNCHANGED, 如果给定的item在adapter当中指定位置不再呈现了,那么就返回POSITION_NONE。默认返回的是POSITION_UNCHANGED。
OK, 导致这个问题的一个主要原因我们已经知道了,所以,默认我们是要重写这个方法的,不然总是返回POSITION_UNCHANGED,那当然是不会更新的了。
其实在使用viewpager包含普通view界面的时候我们应该会经常遇到这个问题的。那么, 这个问题的解决思路就有了: 我们就按照它要求的意思来实现当position发生变化的item我们都返回POSITION_NONE,而position没有发生变化的item我们就返回POSITION_UNCHANGED。
那怎么来实现呢,我们简单来想一下,首先我要记录更新之前的每个item对应的position,然后在更新Fragment列表数据之后,我们再把当前的每一个item的position跟之前的去比对一遍,这样我们就能知道到底哪个item的position发生了变化,哪个的position依然没变了。当然前提是比对的item是相同的item, 如果更新之后item都不存在了,那自然要返回POSITION_NONE了。
好,我们这里就简单的思路设想一下,后面我会给出完整代码。
到这里包含普通view的viewpager的adapter刷新问题应该可以解决了,注意,这里很多人的暴力做法是在getItemPosition()当中直接返回POSITION_NONE,这样不是不可以,只不过这样做会默认把所有的view都重新销毁重建,那肯定不是我想要的理想的情况。
接下来再看另一个方法:
这个是在FragmentPagerAdapter的源码当中的,可以看到在instantiateItem()方法的内部,它是这样做的:根据tag查找对应的Fragment, 如果找到,那么就通过当前的Transaction进行attach操作,这个fragment就会显示了,如果没有找到呢,就去getItem()从你的Fragment列表中获取一个然后Transaction进行add操作。
所以看到这里就恍然大悟了,为啥我list里面的fragment都换了新的了但就是不刷新呢,问题就在这里了,只要它能findFragmentByTag找得到那么就不会用你的列表中的fragment, 还是用之前的。
那么,到这里首先想到的就是,我们在更换或者删除列表中对应的Fragment时,同时也要将该Fragment从Transaction当中移除,这样就能够确保在刷新数据时adpater会从我们更新后的list中去获取fragment而不是用之前缓存的。
是不是这样?对不对?嗯,应该是没有问题的,好,想到这里那么我们就可以了,加上前面getItemPosition()的思路,应该是能够解决问题的了。假设你按照前面的思路完善了FragmentPagerAdapter的代码并准备测试(或者你可以直接往下拖查看完整的代码),你会悲剧的发现,在更换某一个fragment的时候是没有问题的,但是在删除某一个fragment时是会出现问题的,会发生crash! 抛出如下异常:
哎,没办法,江湖就是如此险恶,到处都是坑。。
那么究竟为什么发生crash呢,如果你查看该crash异常栈,我们可以在源码中搜素一下找到:
没错,就是在高亮的这一行,如果你按照前面介绍的方法写好FragmentPagerAdapter 运行测试了,你就会发现抛出”Can’t change tag of fragment “的异常,我们可以发现上述的异常是在beginTransaction()之后进行add操作发生的,异常出现的判断条件是fragment.mTag != null &&!tag.equals(fragment.mTag),这里的tag就是add时传入的tag参数, 而mTag是要添加的frgament的tag, 这说明这个fragment之前被添加过,因为下面一行fragment.mTag = tag;我们知道只有添加过的fragment的mTag才不会为null。
那问题肯定是跟tag有关了,我们回到instantiateItem()方法的源码,可以看到不管是add操作还是findFragmentByTag时的tag都是通过一个方法生成的:
它默认实现就是简单的返回position,所以tag是由fragment的id+position组成的。
那我们来分析一下,删除的时候为啥会出现”Can’t change tag of fragment “的异常,先画个简图:
此时列表中只剩下A C D三个Fragment, 那么前面提到过,此时getItemPpsition()方法我们应该做的是A对应的Fragment返回POSITION_UNCHANGED, 因为A的位置没有发生变化,而B(已删除)、 C(移位) 、 D(移位) 三个我们应该返回POSITION_NONE,因此我们的adapter在刷新的时候刷新到第二个位置时会再首先去查找对应tag的Fragment:
此时查找的tag是C1,然而找不到,因为C前面add的tag是C2,所以走else, 在else当中就会从我们的列表中去get第1个item,那取到的自然是C,然后对C进行add操作,这时又会生成C对应的tag传入add()方法,但是此时,注意了,生成C的tag的方法生成的结果是C1(fragment的id+当前position),分析到这里你可能发现了,前面我们的C是被add过的,所以之前C的mTag是C2,到了这里add操作时要变成C1了!
所以跟着源码走进去自然就符合前面“Can’t change tag of fragment “异常的判断条件fragment.mTag != null &&!tag.equals(fragment.mTag),我们的C之前的mTag不为空并且C1 != C2,所以中标了!
那么解决问题的方法,首先想到的是为每一个Fragment设置一个唯一的tag值,但是mTag在Fragment源码中是protected的,我们是不能改的。。。所以只能去改生成tag的方法makeFragmentName()了,但是一看这个方法又是private的,又不能改。。。。我MMP…好吧,再看,因为makeFragmentName()方法用到了getItemId()的返回值,而getItemId()我们是可以重写的,所以那去只能改getItemId()方法了:
public long getItemId(int position) {
// return position;
return 我们自定义的可以确定当前item的唯一值;
}
因为前面提到过getItemId()方法默认返回的是position,所以我们这个方法要修改一下,返回一个唯一的值,一个可以标志这个fragment的唯一值就可以了,这样在删除操作position发生变化之后,C的tag值经过makeFragmentName()生成的结果总是C+uniqueId, 所以应该不会有问题了。
好了,至此所有问题思路解决完毕,贴一下完善FragmentPagerAdapter的完整代码:
/**
* 加载显示Fragment的ViewPagerAdapter基类
* 提供可以刷新的方法
*
* @author Fly
* @e-mail 1285760616@qq.com
* @time 2018/3/22
*/
public class BaseFragmentPagerAdapter extends FragmentPagerAdapter {
private List<BaseFragment> mFragmentList;
private FragmentManager mFragmentManager;
/**下面两个值用来保存Fragment的位置信息,用以判断该位置是否需要更新*/
private SparseArray<String> mFragmentPositionMap;
private SparseArray<String> mFragmentPositionMapAfterUpdate;
public BaseFragmentPagerAdapter(FragmentManager fm, List<BaseFragment> fragments) {
super(fm);
mFragmentList = fragments;
mFragmentManager = fm;
mFragmentList = fragments;
mFragmentPositionMap = new SparseArray<>();
mFragmentPositionMapAfterUpdate = new SparseArray<>();
setFragmentPositionMap();
setFragmentPositionMapForUpdate();
}
/**
* 保存更新之前的位置信息,用<hashCode, position>的键值对结构来保存
*/
private void setFragmentPositionMap() {
mFragmentPositionMap.clear();
for (int i = 0; i < mFragmentList.size(); i++) {
mFragmentPositionMap.put(Long.valueOf(getItemId(i)).intValue(), String.valueOf(i));
}
}
/**
* 保存更新之后的位置信息,用<hashCode, position>的键值对结构来保存
*/
private void setFragmentPositionMapForUpdate() {
mFragmentPositionMapAfterUpdate.clear();
for (int i = 0; i < mFragmentList.size(); i++) {
mFragmentPositionMapAfterUpdate.put(Long.valueOf(getItemId(i)).intValue(), String.valueOf(i));
}
}
/**
* 在此方法中找到需要更新的位置返回POSITION_NONE,否则返回POSITION_UNCHANGED即可
*/
@Override
public int getItemPosition(Object object) {
int hashCode = object.hashCode();
//查找object在更新后的列表中的位置
String position = mFragmentPositionMapAfterUpdate.get(hashCode);
//更新后的列表中不存在该object的位置了
if (position == null) {
return POSITION_NONE;
} else {
//如果更新后的列表中存在该object的位置, 查找该object之前的位置并判断位置是否发生了变化
int size = mFragmentPositionMap.size();
for (int i = 0; i < size ; i++) {
int key = mFragmentPositionMap.keyAt(i);
if (key == hashCode) {
String index = mFragmentPositionMap.get(key);
if (position.equals(index)) {
//位置没变依然返回POSITION_UNCHANGED
return POSITION_UNCHANGED;
} else {
//位置变了
return POSITION_NONE;
}
}
}
}
return POSITION_UNCHANGED;
}
/**
* 将指定的Fragment替换/更新为新的Fragment
* @param oldFragment 旧Fragment
* @param newFragment 新Fragment
*/
public void replaceFragment(BaseFragment oldFragment, BaseFragment newFragment) {
int position = mFragmentList.indexOf(oldFragment);
if (position == -1) {
return;
}
//从Transaction移除旧的Fragment
removeFragmentInternal(oldFragment);
//替换List中对应的Fragment
mFragmentList.set(position, newFragment);
//刷新Adapter
notifyItemChanged();
}
/**
* 将指定位置的Fragment替换/更新为新的Fragment,同{@link #replaceFragment(BaseFragment oldFragment, BaseFragment newFragment)}
* @param position 旧Fragment的位置
* @param newFragment 新Fragment
*/
public void replaceFragment(int position, BaseFragment newFragment) {
BaseFragment oldFragment = mFragmentList.get(position);
removeFragmentInternal(oldFragment);
mFragmentList.set(position, newFragment);
notifyItemChanged();
}
/**
* 移除指定的Fragment
* @param fragment 目标Fragment
*/
public void removeFragment(BaseFragment fragment) {
//先从List中移除
mFragmentList.remove(fragment);
//然后从Transaction移除
removeFragmentInternal(fragment);
//最后刷新Adapter
notifyItemChanged();
}
/**
* 移除指定位置的Fragment,同 {@link #removeFragment(BaseFragment fragment)}
* @param position
*/
public void removeFragment(int position) {
BaseFragment fragment = mFragmentList.get(position);
//然后从List中移除
mFragmentList.remove(fragment);
//先从Transaction移除
removeFragmentInternal(fragment);
//最后刷新Adapter
notifyItemChanged();
}
/**
* 添加Fragment
* @param fragment 目标Fragment
*/
public void addFragment(BaseFragment fragment) {
mFragmentList.add(fragment);
notifyItemChanged();
}
/**
* 在指定位置插入一个Fragment
* @param position 插入位置
* @param fragment 目标Fragment
*/
public void insertFragment(int position, BaseFragment fragment) {
mFragmentList.add(position, fragment);
notifyItemChanged();
}
private void notifyItemChanged() {
//刷新之前重新收集位置信息
setFragmentPositionMapForUpdate();
notifyDataSetChanged();
setFragmentPositionMap();
}
/**
* 从Transaction移除Fragment
* @param fragment 目标Fragment
*/
private void removeFragmentInternal(BaseFragment fragment) {
FragmentTransaction transaction = mFragmentManager.beginTransaction();
transaction.remove(fragment);
transaction.commitNow();
}
/**
* 此方法不用position做返回值即可破解fragment tag异常的错误
*/
@Override
public long getItemId(int position) {
// 获取当前数据的hashCode,其实这里不用hashCode用自定义的可以关联当前Item对象的唯一值也可以,只要不是直接返回position
return mFragmentList.get(position).hashCode();
}
@Override
public Fragment getItem(int position) {
return mFragmentList.get(position);
}
@Override
public int getCount() {
return mFragmentList.size();
}
public List<BaseFragment> getFragments() {
return mFragmentList;
}
}
好了,现在这个类可以用来实现Fragment列表中的Fragment替换、删除、添加等操作了,并且可以实时刷新adapter, 你可以试验一下。
测试代码:
Activity代码
public class TestActivity extends FragmentActivity implements View.OnClickListener {
List<Fragment> mFragmentList;
ViewPager mViewPager;
public BaseFragmentPagerAdapter mAdapter;
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_test);
mViewPager = findViewById(R.id.vp);
findViewById(R.id.btn_change).setOnClickListener(this);
mFragmentList = new ArrayList<>();
mFragmentList.add(getFg("AAA"));
mFragmentList.add(getFg("BBB"));
mFragmentList.add(getFg("CCC"));
mFragmentList.add(getFg("DDD"));
mAdapter = new BaseFragmentPagerAdapter(getSupportFragmentManager(), mFragmentList);
mViewPager.setAdapter(mAdapter);
}
private TestFragment getFg(String a){
TestFragment fragment = new TestFragment();
fragment.setTest(a);
return fragment;
}
public void onClick(View view) {
TestFragment eee = getFg("EEE");
//新增
mAdapter.addFragment(eee);
//插入
mAdapter.insertFragment(1, eee);
//删除
mAdapter.removeFragment(1);
//删除
mAdapter.removeFragment(mFragmentList.get(1));
//替换
mAdapter.replaceFragment(1, eee);
//替换
mAdapter.replaceFragment(mFragmentList.get(0), eee);
}
}
用到的TestFragment:
public class TestFragment extends Fragment {
private final static String TAG = TestFragment.class.getSimpleName();
private String test;
public View mContentView;
public void setTest(String test) {
this.test = test;
}
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
Log.e(TAG, "onCreate: test = "+test);
}
public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
mContentView = inflater.inflate(R.layout.layout_fg, null);
Log.e(TAG, "onCreateView: test = "+test);
return mContentView;
}
public void onViewCreated(View view, @Nullable Bundle savedInstanceState) {
Log.e(TAG, "onViewCreated: test = "+test);
TextView testText = mContentView.findViewById(R.id.tv_test);
testText.setText(test);
}
public void onActivityCreated(@Nullable Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
Log.e(TAG, "onActivityCreated: test = "+test);
}
public void onDestroy() {
super.onDestroy();
Log.e(TAG, "onDestroy: test = "+test);
}
}
布局文件:
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
>
<android.support.v4.view.ViewPager
android:id="@+id/vp"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
android:layout_gravity="center"
/>
<Button
android:id="@+id/btn_change"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:text="btn_change"
/>
</LinearLayout>
布局文件很简单就是一个viewpager+一个button, 然后我们在Activity当中点击这个button对vp的adapter所使用的fragment列表进行操作,并观察变化。
注意,封装的Adapter类提供了新增、插入、删除、替换几种方法的重载,可以通过指定的位置或者fragment进行操作,在onClick()中测试时,注释其他的情况,只测试一种情况即可。
另外,我们在TestFragment中的生命周期方法中添加了Log日志,以便观察结果。
运行代码测试你会发现,当替换掉列表中的一个Fragment时,左右两边的Fragment生命周期是不会被调用的。这符合我们的预期,因为替换时左右两边的Fragment在viewpager中的位置没有发生变化,所以它们不会被销毁重建。
当你删除或者插入一个Fragment时,当前Fragment后面的Fragment会走重新创建view的生命周期方法,而当前Fragment前面的Fragment不会,这也符合我们的预期,但为啥后面的会重建,而前面的不会?别忘了我们使用的viewpager是有默认预加载当前页面左右两边的view的特性的,所以这个也属于正常的现象,如果viewpager预加载给你造成了困扰,我们可以通过其它方式来避免,当然这是另外的话题了。
参考:
我在寻找解决方案的过程中参考了这个:
https://stackoverflow.com/questions/10396321/remove-fragment-page-from-viewpager-in-android#
推荐阅读:
如果你想要跟大家分享你的文章,欢迎投稿~
图书信息已经收集完毕,今天就会交给出版社了,申请了14个好友,有个没搜索到~~谢谢大家支持。
┏(^0^)┛明天见!